//------------------------------------------------------------------------------
//  d3d9texturestreamer.cc
//  (C) 2010 Radon Labs GmbH
//------------------------------------------------------------------------------
#include "stdneb.h"
#include "d3d9texturestreamer.h"
#include "coregraphics/texture.h"
#include "coregraphics/renderdevice.h"
#include "io/ioserver.h"
#include "ddraw.h"
#include "coregraphics/win360/d3d9types.h"
#include "foundation/util/array.h"

namespace Resources
{
__ImplementClass(Resources::D3D9TextureStreamer, 'D3TS', Resources::StreamResourceLoader);

using namespace CoreGraphics;
using namespace IO;
using namespace Util;

//------------------------------------------------------------------------------
/**
*/
D3D9TextureStreamer::D3D9TextureStreamer()
{
}

//------------------------------------------------------------------------------
/**
*/
D3D9TextureStreamer::~D3D9TextureStreamer()
{
}

//------------------------------------------------------------------------------
/**
    Call this method if something has to be loaded from disk. This may include total
    reuse of another texture but not partial reuse as this can be done by calling
    D3D9TextureStreamer::ReuseMips which doesn't require a file stream.
*/
bool
D3D9TextureStreamer::SetupTexture2DFromStream(const Ptr<IO::Stream>& stream)
{
    const Ptr<Texture>& tex = this->resource.downcast<Texture>();
    if (this->reuseTexture.isvalid())
    {
        n_assert2(tex->GetNumMipLevels() > this->reuseTexture->GetNumMipLevels(), 
            "SetupTexture2DFromStream called but destination texture could be totally generated by"
            " copying texture in memory! call D3D9TextureStreamer::ReuseMips() instead so no "
            "unneeded stream is created!");
    }
#if ENABLE_LOAD_TIMERS
    _setup_timer(LoadingTimerTotal);
    _setup_timer(LoadingTimer1);
    _setup_timer(LoadingTimer2);
    _setup_timer(LoadingTimer3);
    _setup_timer(LoadingTimerCopy);

    _start_timer(LoadingTimerTotal);
#endif
    n_printf("loading resource '%s'\n", stream->GetURI().AsString());

    // --- load ---
    stream->SetAccessMode(Stream::ReadAccess);
    bool streamIsOpen = stream->Open();
    n_assert(streamIsOpen);
    void* srcData = stream->Map();

    // --- decompress ---
    // --- process ---
    // --- lock ---

    // determine how much mips we may reuse from another texture in memory
    // as this method is only called if no reuseTexture is set or a reuseTexture is set and has a smaller mip count
    // we can totally reuse the reuseTexture and not just partially
    // mipsToLoad refers to mips we want to load from file starting after all skippedMips and ending at all reusable mips
    SizeT mipsToLoad = tex->GetNumMipLevels();
    if (this->reuseTexture.isvalid())
    {
        mipsToLoad -= this->reuseTexture->GetNumMipLevels();
    }

    IndexT mipIdx;
    D3DLOCKED_RECT dummy;
    Array<D3DLOCKED_RECT> lockedRects(mipsToLoad, 8, dummy);
    this->LockSurfaces(lockedRects, tex, mipsToLoad);

    // --- copy ---

    // width and height of current mipLevel
    uint curWidth = tex->GetWidth();
    uint curHeight = tex->GetHeight();

    // as we need to start with height and width of file' first mip map to skip mips in file
    // we need to multiply by 2 for each skipped mip
    // @todo: check if this can be done more efficiently using n_pow()
    for (mipIdx = 0; mipIdx < tex->GetSkippedMips(); mipIdx++)
    {
        curHeight = curHeight << 1;
        curWidth = curWidth << 1;
    }

    D3DFORMAT format = Win360::D3D9Types::AsD3D9PixelFormat(tex->GetPixelFormat());
    //uint numBytes = 0, rowBytes = 0, numRows = 0;
    uint numRows = 0;
    //uint rowIdx;
    int pitchSize = 0;

    // skip X number of mip levels
    uint bytesToSkip = 0, surfaceBytes = 0;
    for (mipIdx = 0; mipIdx < tex->GetSkippedMips(); mipIdx++)
    {
        this->GetSurfaceInfo(curWidth, curHeight, format, &surfaceBytes, 0, 0);
        bytesToSkip += surfaceBytes;
        curWidth = curWidth >> 1;
        curHeight = curHeight >> 1;
    }

    // pointer to start of data (start + skip header infos + skipped mips)
    BYTE* texData = (BYTE*)srcData + sizeof(DWORD) + sizeof(DDSURFACEDESC2) + bytesToSkip;

    for (mipIdx = 0; mipIdx < mipsToLoad; mipIdx++)
    {
        numRows = this->GetNumRows(curHeight, format);
       
        BYTE* destData = (BYTE*)lockedRects[mipIdx].pBits;
        Memory::Copy(texData, destData, lockedRects[mipIdx].Pitch * numRows);

        texData += lockedRects[mipIdx].Pitch * numRows;
        curWidth = curWidth >> 1;
        if (0 == curWidth)
        {
            curWidth = 1;
        }
        curHeight = curHeight >> 1;
        if (0 == curHeight)
        {
            curHeight = 1;
        }
    }

    if (this->reuseTexture.isvalid())
    {
        this->ReuseMips();
    }

    // --- unlock ---
    this->UnlockSurfaces(tex);

#if ENABLE_LOAD_TIMERS
    _stop_timer(LoadingTimerTotal);
    _reset_accum_timer(LoadingTimer2);
    _reset_accum_timer(LoadingTimer3);
    _reset_accum_timer(LoadingTimerCopy);
    n_printf("total time needed for loading: %f\n", LoadingTimerTotal->GetSample());
    n_printf("time for copying total: %f\n", LoadingTimer1->GetSample());
    n_printf("time for MemCopy: %f\n", LoadingTimerCopy->GetSample());
    n_printf("time for timer2: %f\n", LoadingTimer2->GetSample());
    n_printf("time for timer3: %f\n", LoadingTimer3->GetSample());

    _discard_timer(LoadingTimerTotal);
    _discard_timer(LoadingTimer1);
    _discard_timer(LoadingTimer2);
    _discard_timer(LoadingTimer3);
    _discard_timer(LoadingTimerCopy);
#endif
    return true;
}

//------------------------------------------------------------------------------
/**
*/
bool
D3D9TextureStreamer::SetupTextureCubeFromStream(const Ptr<IO::Stream>& stream)
{
    const Ptr<Texture>& tex = this->resource.downcast<Texture>();
    if (this->reuseTexture.isvalid())
    {
        n_printf("NOTE: Reuse of CubeTextures is not supported yet");
        //n_assert2(tex->GetNumMipLevels() > this->reuseTexture->GetNumMipLevels(), 
        //    "SetupTextureCubeFromStream called but destination texture could be totally generated by"
        //    " copying texture in memory! call D3D9TextureStreamer::ReuseMips() instead so no "
        //    "unneeded stream is created!");
    }
    n_printf("loading resource '%s'\n", stream->GetURI().AsString());

    // --- load ---
    stream->SetAccessMode(Stream::ReadAccess);
    n_assert(stream->Open());
    void* srcData = stream->Map();

    // --- decompress ---
    // --- process ---
    // --- lock ---

    // determine how much mips we may reuse from another texture in memory
    // as this method is only called if no reuseTexture is set or a reuseTexture is set and has a smaller mip count
    // we can totally reuse the reuseTexture and not just partially
    // mipsToLoad refers to mips we want to load from file starting after all skippedMips and ending at all reusable mips
    SizeT mipsToLoad = tex->GetNumMipLevels();
    //if (this->reuseTexture.isvalid())
    //{
    //    mipsToLoad -= this->reuseTexture->GetNumMipLevels();
    //}

    D3DLOCKED_RECT dummy;
    Array<D3DLOCKED_RECT> lockedRects(mipsToLoad * 6, 8, dummy);
    this->LockSurfaces(lockedRects, tex);

    // --- copy ---

    // width and height of current mipLevel
    uint curWidth = tex->GetWidth();
    uint curHeight = tex->GetHeight();

    // as we need to start with height and width of file' first mip map to skip mips in file
    // we need to multiply by 2 for each skipped mip (this value is per face!)
    // @todo: check if this can be done more efficiently using n_pow()
    IndexT mipIdx, faceIdx;
    for (mipIdx = 0; mipIdx < tex->GetSkippedMips(); mipIdx++)
    {
        curHeight = curHeight << 1;
        curWidth = curWidth << 1;
    }

    D3DFORMAT format = Win360::D3D9Types::AsD3D9PixelFormat(tex->GetPixelFormat());
    uint numRows = 0;
    int pitchSize = 0;

    // skip X number of mip levels
    uint bytesToSkip = 0, surfaceBytes = 0;
    // compute bytes to skip per face (skipped mips)
    for (mipIdx = 0; mipIdx < tex->GetSkippedMips(); mipIdx++)
    {
        this->GetSurfaceInfo(curWidth, curHeight, format, &surfaceBytes, 0, 0);
        bytesToSkip += surfaceBytes;
        curWidth = curWidth >> 1;
        curHeight = curHeight >> 1;
    }

    // compute bytes of a whole face (to skip to next face)
    // therefore use previously computed bytes to skip and add non-skipped mips
    uint faceBytes = bytesToSkip;
    // save width and height of highest mip map we want to load from file
    uint initialHeight = curHeight;
    uint initialWidth = curWidth;

    for (mipIdx = 0; mipIdx < tex->GetNumMipLevels(); mipIdx++)
    {
        this->GetSurfaceInfo(curWidth, curHeight, format, &faceBytes, 0, 0);
        faceBytes += surfaceBytes;
        curWidth = curWidth >> 1;
        curHeight = curHeight >> 1;
    }

    // copy mipMaps of each face
    // pointer to start of data (skip header infos)
    BYTE* startData = (BYTE*)srcData + sizeof(DWORD) + sizeof(DDSURFACEDESC2);

    for (faceIdx = 0; faceIdx < 6; faceIdx++)
    {
        BYTE* texData = startData + bytesToSkip + faceBytes * faceIdx;
        // reset height and width for new face
        curHeight = initialHeight;
        for (mipIdx = 0; mipIdx < mipsToLoad; mipIdx++)
        {
            numRows = this->GetNumRows(curHeight, format);

            BYTE* destData = (BYTE*)lockedRects[mipIdx + faceIdx * mipsToLoad].pBits;
            Memory::Copy(texData, destData, lockedRects[mipIdx + faceIdx * mipsToLoad].Pitch * numRows);

            texData += lockedRects[mipIdx].Pitch * numRows;
            curHeight = curHeight >> 1;
            if (0 == curHeight)
            {
                curHeight = 1;
            }
        }
    }

    // --- unlock ---
    this->UnlockSurfaces(tex);

    return true;
}

//------------------------------------------------------------------------------
/**
This method actually setups the Texture object from the data in the stream.
*/
bool
D3D9TextureStreamer::SetupResourceFromStream(const Ptr<Stream>& stream)
{
    bool result = false;
    const Ptr<Texture>& tex = this->resource.downcast<Texture>();
    if (tex->GetType() == Texture::Texture2D)
    {
        result = this->SetupTexture2DFromStream(stream);
        if (this->reuseTexture.isvalid())
        {
            this->reuseTexture->SetState(Texture::Initial);
            this->reuseTexture->Unlock();
        }
        tex->Unlock();
    }
    else if (tex->GetType() == Texture::TextureCube)
    {
        result = this->SetupTextureCubeFromStream(stream);
    }
    return result;
}

//------------------------------------------------------------------------------
/**
    NOTE: Ensure surfaces which are overwritten by this method are NOT LOCKED
    otherwise D3DXLoadSurfaceFromSurface may not work correctly (HRESULT is S_OK but texture is incorrect anyway).
*/
bool
D3D9TextureStreamer::ReuseMips()
{
    n_assert(this->reuseTexture.isvalid());
    Ptr<Texture> tex = this->resource.downcast<Texture>();
    n_assert2(tex->GetType() == Texture::Texture2D, "Error: cube or volume textures are currently not supported for mip map reusage.");
    n_printf("reusing mips of texture '%s' (%i to %i)\n", tex->GetResourceId().Value(), this->reuseTexture->GetNumMipLevels(), tex->GetNumMipLevels());

    // copy
    IndexT srcMipIdx = 0;
    IndexT dstMipIdx = 0;
    if (this->reuseTexture->GetNumMipLevels() > tex->GetNumMipLevels())
    {
        srcMipIdx = this->reuseTexture->GetNumMipLevels() - tex->GetNumMipLevels();
    }
    else
    {
        dstMipIdx = tex->GetNumMipLevels() - this->reuseTexture->GetNumMipLevels();
    }
    while (dstMipIdx < tex->GetNumMipLevels())
    {
        n_assert(srcMipIdx < this->reuseTexture->GetNumMipLevels());

        IDirect3DSurface9* srcSurface;
        IDirect3DSurface9* dstSurface;
        this->reuseTexture->GetD3D9Texture()->GetSurfaceLevel(srcMipIdx, &srcSurface);
        tex->GetD3D9Texture()->GetSurfaceLevel(dstMipIdx, &dstSurface);
        HRESULT result = D3DXLoadSurfaceFromSurface(dstSurface, 0, 0, srcSurface, 0, 0, D3DX_FILTER_NONE, 0);
        n_assert(SUCCEEDED(result));

        dstMipIdx++;
        srcMipIdx++;
    }

    return true;
}

//------------------------------------------------------------------------------
/**
*/
void
D3D9TextureStreamer::LockSurfaces(Array<D3DLOCKED_RECT>& lockedRects, const Ptr<Texture>& tex, SizeT numMipsToLock)
{
    IndexT mipIdx;
    if (InvalidIndex == numMipsToLock)
    {
        numMipsToLock = tex->GetNumMipLevels();
    }
    HRESULT result;

    if (Texture::Texture2D == tex->GetType())
    {
        for (mipIdx = 0; mipIdx < numMipsToLock; mipIdx++)
        {
            result = tex->GetD3D9Texture()->LockRect(mipIdx, &lockedRects[mipIdx], 0, 0);
            n_assert(SUCCEEDED(result));
        }
    }
    else if (Texture::TextureCube == tex->GetType())
    {
        IndexT faceIdx;
        for (faceIdx = 0; faceIdx < 6; faceIdx++)
        {
            for (mipIdx = 0; mipIdx < numMipsToLock; mipIdx++)
            {
                result = tex->GetD3D9CubeTexture()->LockRect((D3DCUBEMAP_FACES)faceIdx, mipIdx, &lockedRects[mipIdx + faceIdx * numMipsToLock], 0, 0);
                n_assert(SUCCEEDED(result));
            }
        }
    }
    else
    {
        n_error("Error: Texture type is not supported.");
    }
}

//------------------------------------------------------------------------------
/**
*/
void
D3D9TextureStreamer::UnlockSurfaces(const Ptr<Texture>& tex)
{
    IndexT mipIdx;
    SizeT numMips = tex->GetNumMipLevels();
    HRESULT result;
    if (Texture::Texture2D == tex->GetType())
    {
        for (mipIdx = 0; mipIdx < tex->GetNumMipLevels(); mipIdx++)
        {
            result = tex->GetD3D9Texture()->UnlockRect(mipIdx);
            n_assert(SUCCEEDED(result));
        }
    }
    else if (Texture::TextureCube == tex->GetType())
    {
        IndexT faceIdx;
        for (faceIdx = 0; faceIdx < 6; faceIdx++)
        {
            for (mipIdx = 0; mipIdx < numMips; mipIdx++)
            {
                result = tex->GetD3D9CubeTexture()->UnlockRect((D3DCUBEMAP_FACES)faceIdx, mipIdx);
                n_assert(SUCCEEDED(result));
            }
        }
    }
    else
    {
        n_error("Error: Texture type is not supported.");
    }    
}


//------------------------------------------------------------------------------
/**
*/
uint
D3D9TextureStreamer::BitsPerPixel(D3DFORMAT fmt) const
{
    switch( fmt )
    {
    case D3DFMT_A32B32G32R32F:
        return 128;

    case D3DFMT_A16B16G16R16:
    case D3DFMT_Q16W16V16U16:
    case D3DFMT_A16B16G16R16F:
    case D3DFMT_G32R32F:
        return 64;

    case D3DFMT_A8R8G8B8:
    case D3DFMT_X8R8G8B8:
    case D3DFMT_A2B10G10R10:
    case D3DFMT_A8B8G8R8:
    case D3DFMT_X8B8G8R8:
    case D3DFMT_G16R16:
    case D3DFMT_A2R10G10B10:
    case D3DFMT_Q8W8V8U8:
    case D3DFMT_V16U16:
    case D3DFMT_X8L8V8U8:
    case D3DFMT_A2W10V10U10:
    case D3DFMT_D32:
    case D3DFMT_D24S8:
    case D3DFMT_D24X8:
    case D3DFMT_D24X4S4:
    case D3DFMT_D32F_LOCKABLE:
    case D3DFMT_D24FS8:
    case D3DFMT_INDEX32:
    case D3DFMT_G16R16F:
    case D3DFMT_R32F:
        return 32;

    case D3DFMT_R8G8B8:
        return 24;

    case D3DFMT_A4R4G4B4:
    case D3DFMT_X4R4G4B4:
    case D3DFMT_R5G6B5:
    case D3DFMT_L16:
    case D3DFMT_A8L8:
    case D3DFMT_X1R5G5B5:
    case D3DFMT_A1R5G5B5:
    case D3DFMT_A8R3G3B2:
    case D3DFMT_V8U8:
    case D3DFMT_CxV8U8:
    case D3DFMT_L6V5U5:
    case D3DFMT_G8R8_G8B8:
    case D3DFMT_R8G8_B8G8:
    case D3DFMT_D16_LOCKABLE:
    case D3DFMT_D15S1:
    case D3DFMT_D16:
    case D3DFMT_INDEX16:
    case D3DFMT_R16F:
    case D3DFMT_YUY2:
        return 16;

    case D3DFMT_R3G3B2:
    case D3DFMT_A8:
    case D3DFMT_A8P8:
    case D3DFMT_P8:
    case D3DFMT_L8:
    case D3DFMT_A4L4:
        return 8;

    case D3DFMT_DXT1:
        return 4;
    case D3DFMT_DXT2:
    case D3DFMT_DXT3:
    case D3DFMT_DXT4:
    case D3DFMT_DXT5:
        return  8;

    default:
        assert( FALSE ); // unhandled format
        return 0;
    }
}

//------------------------------------------------------------------------------
/**
    This method is taken from DirectX SDK Aug 2007 (contained in newer SDKs, too).
*/
void
D3D9TextureStreamer::GetSurfaceInfo(uint width, uint height, D3DFORMAT fmt, uint* pNumBytes, uint* pRowBytes, uint* pNumRows) const
{
    uint numBytes = 0;
    uint rowBytes = 0;
    uint numRows = 0;

    // From the DXSDK docs:
    //
    //     When computing DXTn compressed sizes for non-square textures, the 
    //     following formula should be used at each mipmap level:
    //
    //         max(1, width ?4) x max(1, height ?4) x 8(DXT1) or 16(DXT2-5)
    //
    //     The pitch for DXTn formats is different from what was returned in 
    //     Microsoft DirectX 7.0. It now refers the pitch of a row of blocks. 
    //     For example, if you have a width of 16, then you will have a pitch 
    //     of four blocks (4*8 for DXT1, 4*16 for DXT2-5.)"

    if(fmt == D3DFMT_DXT1 || fmt == D3DFMT_DXT2 || fmt == D3DFMT_DXT3 || fmt == D3DFMT_DXT4 || fmt == D3DFMT_DXT5)
    {
        // Note: we support width and/or height being 0 in order to compute
        // offsets in functions like CBufferLockEntry::CopyBLEToPerfectSizedBuffer().
        int numBlocksWide = 0;
        if(width > 0)
            numBlocksWide = max(1, width / 4);
        int numBlocksHigh = 0;
        if(height > 0)
            numBlocksHigh = max(1, height / 4);
        //int numBlocks = numBlocksWide * numBlocksHigh;
        int numBytesPerBlock = (fmt == D3DFMT_DXT1 ? 8 : 16);
        rowBytes = numBlocksWide * numBytesPerBlock;
        numRows = numBlocksHigh;
    }
    else
    {
        uint bpp = this->BitsPerPixel( fmt );
        rowBytes = (width * bpp + 7) / 8; // round up to nearest byte
        numRows = height;
    }
    numBytes = rowBytes * numRows;
    if(pNumBytes != NULL)
        *pNumBytes = numBytes;
    if(pRowBytes != NULL)
        *pRowBytes = rowBytes;
    if(pNumRows != NULL)
        *pNumRows = numRows;
}

//------------------------------------------------------------------------------
/**
*/
uint
D3D9TextureStreamer::GetNumRows(uint height, D3DFORMAT fmt) const
{
    if(height > 0 &&
        (fmt == D3DFMT_DXT1 || fmt == D3DFMT_DXT2 || fmt == D3DFMT_DXT3 || fmt == D3DFMT_DXT4 || fmt == D3DFMT_DXT5))
    {
        return max( 1, height / 4 );
    }
    else
    {
        return height;
    }
}

//------------------------------------------------------------------------------
/**
*/
void
D3D9TextureStreamer::Reset()
{
    if (this->reuseTexture.isvalid())
    {
        this->reuseTexture->Unlock();
        this->reuseTexture = 0;
    }
    if (this->resource.isvalid())
    {
        this->resource->Unlock();
    }
    ResourceLoader::Reset();
}

//------------------------------------------------------------------------------
/**
    If we can copy the Texture' data totally from another Texture in memory we don't need
    to create a file stream.
*/
bool
D3D9TextureStreamer::OnLoadRequested()
{
    if (this->reuseTexture.isvalid())
    {
#ifdef NEBULA3_RESOURCE_DEBUG
        n_assert2(tex->GetNumMipLevels() != this->reuseTexture->GetNumMipLevels(),
            "Error: number of mips of target texture '%s' equals number of mips of source-texture '%s'."
            "if copying shall be enabled remove this error",
            tex->GetResourceId().AsString().AsCharPtr(), this->reuseTexture->GetResourceId().AsString().AsCharPtr());
#endif
        const Ptr<Texture>& tex = this->resource.downcast<Texture>();
        if (tex->GetNumMipLevels() < this->reuseTexture->GetNumMipLevels())
        {
            if (this->ReuseMips())
            {
                this->SetState(Resource::Loaded);
                return true;
            }
            else
            {
                this->SetState(Resource::Failed);
                return false;
            }
        }
    }   

    return StreamResourceLoader::OnLoadRequested();
}
} // namespace Resources
//------------------------------------------------------------------------------